package protocol.channels.control;

import java.io.IOException;
import java.net.InetAddress;
import java.net.SocketException;
import java.net.UnknownHostException;

import exceptions.ArgumentException;
import files.FileHandler;
import general.SocketHelper;

import protocol.Frame;
import protocol.Receiver;
import protocol.Sender;
import protocol.control.Packet;
import protocol.control.ReceiveInfoPacket;
import protocol.control.ReceiveRequestPacket;
import protocol.control.SendInfoPacket;
import protocol.control.SendRequestPacket;
import virtualdatagramsocket.VirtualDatagramSocket;

public class ReceiverControlChannel extends ControlChannel implements Runnable  {
	
	public void listen(String host, int port) throws SocketException, UnknownHostException {
		VirtualDatagramSocket socket = new VirtualDatagramSocket(
			port, InetAddress.getByName(host));
		try {
			socket.setPacketLossRate(0);
		} catch (Exception e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		this.setSocket(socket);
	}

	public void run() {
		/**
		 * The current states of the 3-way handshakes.
		 */
		int synHandshakeState = 0; // used for connection initialization
		//int finHandshakeState = 0; // used for connection shutdown
		try {
			while (!this.getSocket().isClosed()) {
				/**
				 * All other incoming data is dropped 
				 * until the handshake has been completed.
				 */
				if (synHandshakeState < 3) {
					if (this.handshake(synHandshakeState)) {
						//System.out.println("Receiver succeeded in state " + synHandshakeState);	
						synHandshakeState++;
						//System.out.println("Receiver entering state " + synHandshakeState);
					} else {
						//System.out.println("Receiver failure in state " + synHandshakeState);
						/**
						 * If the last ACK packet was failed to be received,
						 * the previously sent SYN/ACK packet was lost. Resend it
						 */
						if (synHandshakeState == 2) synHandshakeState = 1;
					}
				} else {
					//System.out.println("Receiver completed 3-way handshake!");
					Frame p = this.receive(0); // timeout 0 -> infinite time
					// Type 1 -> Send request packet
					if (((Packet)p).getType() == 1) 
						this.handleSendRequest((SendRequestPacket)((Packet)p).getData());
					// Type 3 -> Receive request packet
					if (((Packet)p).getType() == 3) 
						this.handleReceiveRequest((ReceiveRequestPacket)((Packet)p).getData());
				}
				Thread.yield();
			}
		} catch (IOException e) {
			/**
			 * Close the socket and show the error message
			 */
			//this.getSocket().close();
			System.out.println(e.toString());
		}
		System.out.println("Receiver control channel was closed");
	}

	/**
	 * Used to send and received the 3-way handshake packets
	 * @param state 0 = receive SYN, 1 = send SYN/ACK, 2 = receive ACK
	 * @throws IOException if any I/O errors occur
	 * @throws IllegalStateException If the given state is not 0, 1 or 2.
	 */
	private boolean handshake(int state) throws IOException {
		if (state == 0) {
			/**
			 * receive SYN
			 */
			//System.out.println("Receiver waiting for SYN ...");
			if (Handshake.isHandshakeSynPacket(this.receive(5))) {
				//System.out.println("Receiver received SYN!");
				return true;
			} else {
				//System.out.println("Receiver failed to receive SYN");
				return false;
			}
		} else if (state == 1) {
			/**
			 * send SYN/ACK
			 */
			//System.out.println("Receiver sending SYN/ACK");
			this.send(Handshake.getHandshakeSynAckPacket(), this.sourceAddress, this.sourcePort);
			return true;
		} else if (state == 2) {
			/**
			 * receive ACK
			 */
			//System.out.println("Receiver waiting for ACK ...");
			if (Handshake.isHandshakeAckPacket(this.receive(5))) {
				//System.out.println("Receiver received ACK!");
				return true;
			} else {
				//System.out.println("Receiver failed to received ACK");
				return false;
			}
		} else {
			throw new IllegalStateException("Invalid state number");
		}
	}
	
	/**
	 * Handles send request packets
	 * @param sendRequestPacket The packet to be handled 
	 * @throws IOException if any I/O errors occur
	 */
	private void handleSendRequest(SendRequestPacket sendRequestPacket) throws IOException {
		Packet packet = new Packet();
		packet.setType((byte)2);
		SendInfoPacket p = new SendInfoPacket(); // the response packet
		// check does the file exist
		p.setAvailable(FileHandler.exists(sendRequestPacket.getFilename()));
		// get the file size (0 if it does not exist)
		p.setFilesize(FileHandler.getFilesize(sendRequestPacket.getFilename()));
		// copy the file name to the response
		packet.setData(p);
		try {
			p.setFilename(sendRequestPacket.getFilename());
		} catch (ArgumentException e) {
			// packet parsing is not correctly implemented.
		}
		// send the response
		this.send(packet, this.sourceAddress, this.sourcePort);
		/**
		 * Start the file transfer in the data channel
		 * if the requested file existed
		 */
		if (FileHandler.exists(sendRequestPacket.getFilename())) {
			/**
			 * Open a new socket in a random port 
			 * for the data transfer
			 */
			VirtualDatagramSocket dataTransferSocket = 
				SocketHelper.open(this.getSocket().getLocalAddress().getHostName());
			/**
			 * Create a new data channel for sending the file
			 */
			Sender sender = new Sender(
				dataTransferSocket, 
				sendRequestPacket.getFilename(), 
				this.sourceAddress, 
				sendRequestPacket.getPort());
			/**
			 * Start the transfer
			 */
			new Thread(sender).start();
		}
	}
	
	/**
	 * Used to handle receive request packets
	 * @param packet The packet to be handled
	 * @throws IOException If any I/O errors occur
	 */
	private void handleReceiveRequest(ReceiveRequestPacket receiveRequestPacket) throws IOException {
		/**
		 * Open a new socket in a random port 
		 * for the data transfer
		 */
		VirtualDatagramSocket dataTransferSocket = 
			SocketHelper.open(this.getSocket().getLocalAddress().getHostName());
		/**
		 * Create a new data channel
		 */
		Receiver receiver = new Receiver(dataTransferSocket, receiveRequestPacket.getFilename());
		/**
		 * Assemble the response packet
		 */
		Packet packet = new Packet();
		packet.setType((byte)4);
		ReceiveInfoPacket receiveInfoPacket = new ReceiveInfoPacket();
		receiveInfoPacket.setAllowed(true);
		receiveInfoPacket.setFilename(receiveRequestPacket.getFilename());
		try {
			receiveInfoPacket.setPort(dataTransferSocket.getLocalPort());
		} catch (ArgumentException e) {
			// Never happens if SocketHelper is properly implemented
			System.err.println(String.format("ERROR: %s", e.toString()));
			System.err.println("Program closing..");
			System.exit(0);
		}
		packet.setData(receiveInfoPacket);
		/**
		 * Send the response packet
		 */
		this.send(packet, this.sourceAddress, this.sourcePort);
		/**
		 * Start listening for incoming data in the data channel
		 */
		new Thread(receiver).start();

	}
	
}
